﻿
namespace HSS.Interlink.Web
{
	#region Using Directives
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;
	using System.Threading;
	using System.Web;
	#endregion

	#region DiskFileCache
	/// <summary>
	/// A custom <see cref="ITransmitFileCache"/> for temporarily storing transmittable files on disk.
	/// </summary>
	public sealed class DiskFileCache : ITransmitFileCache
	{
		#region Fields

		private const string DiskFileCacheFolder = "_diskFileCache";
		private static object instanceLock = new object();

		private Hashtable fileLocks = new Hashtable();
		private object purgeQueuedLock = new object();
		private bool purgeQueued;

		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		private DiskFileCache()
		{
			_lastPurge = DateTime.Now;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets the current <see cref="DiskFileCache"/> instance.
		/// </summary>
		public static DiskFileCache Current
		{
			get
			{
				if (_instance == null)
				{
					lock (instanceLock)
					{
						if (_instance == null)
						{
							_instance = new DiskFileCache();
						}
					}
				}
				return _instance;
			}
		} private static DiskFileCache _instance;
		/// <summary>
		/// Gets the absolute path to the directory where the cached files are stored.
		/// </summary>
		public string StoreDirectory
		{
			get
			{
				if (!_storeDirectorySet)
				{
					_storeDirectorySet = true;
					_storeDirectory = InterlinkUtilities.GetTempFolder(DiskFileCacheFolder);
				}
				return _storeDirectory;
			}
			private set
			{
				if (string.IsNullOrEmpty(value))
					throw new ArgumentNullException("value");
				_storeDirectorySet = true;
				_storeDirectory = value;
			}
		} string _storeDirectory; bool _storeDirectorySet;
		/// <summary>
		/// Gets or sets if auto purging is enabled. Default: True
		/// </summary>
		public bool AutoPurge
		{
			get { return _autoPurge; }
			set { _autoPurge = value; }
		} bool _autoPurge = true;
		/// <summary>
		/// Gets or sets the purge interval.
		/// </summary>
		public TimeSpan PurgeInterval
		{
			get
			{
				return _purgeInterval;
			}
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				if (value.Ticks < 0)
					throw new ArgumentOutOfRangeException("value");
				_purgeInterval = value;
			}
		} TimeSpan _purgeInterval = new TimeSpan(0, 10, 0);
		/// <summary>
		/// Gets or sets the last purge time stamp.
		/// </summary>
		private DateTime LastPurge
		{
			get
			{
				if (_lastPurge == null)
				{
					_lastPurge = DateTime.Now.Subtract(PurgeInterval);
				}
				return _lastPurge;
			}
			set
			{
				_lastPurge = value;
			}
		} DateTime _lastPurge;
		#endregion

		#region Methods

		#region ITransmitFileCache Members
		/// <summary>
		/// Attempt to transmit the cached file if it exists.
		/// </summary>
		/// <param name="fileName">The name of the file to try and transmit.</param>
		/// <param name="transmitter">An <see cref="IFileTransmit"/> instance responsible for delivering the file.</param>
		/// <param name="attachment">true to send with Attachment Disposition; otherwise false for Inline Disposition.</param>
		/// <param name="contentTypeOverride">Optional parameter to specify the Content-Type for the response; otherwise an empty string to allow automatic Content-Type resolution based on file extension.</param>
		/// <returns>true if the file was found and transmitted; otherwise false.</returns>
		public bool TryTransmitIfExists(string fileName, IFileTransmit transmitter, bool attachment, string contentTypeOverride)
		{
			if (AutoPurge)
				PrivateQueueAutoPurge(DateTime.Now.Subtract(LastPurge));

			lock (this.GetFileLockObject(fileName))
			{
				var path = Path.Combine(StoreDirectory, fileName);
				var fi = new FileInfo(path);
				if (fi.Exists)
				{
					transmitter.TransmitLocalFile(fi.FullName, fi.Name, fi.Length, attachment, contentTypeOverride);
					return true;
				}
				else
				{
					return false;
				}
			}
		}
		#endregion

		#region IFileCache Members

		/// <summary>
		/// Adds the provided file bytes to the file cache.
		/// </summary>
		/// <param name="fileName">The name of the file being cached.</param>
		/// <param name="content">The file contents to cache.</param>
		/// <returns>The absolute path the cached file.</returns>
		/// <exception cref="ArgumentNullException">The 'content' parameter is null.</exception>
		public string Add(string fileName, byte[] content)
		{
			lock (GetFileLockObject(fileName))
			{
				if (null == content || content.Length == 0)
					throw new ArgumentNullException("content");
				var path = Path.Combine(StoreDirectory, fileName);
				if (File.Exists(path))
				{
					File.Delete(path);
					Thread.Sleep(0);
				}
				File.WriteAllBytes(path, content);
				return path;
			}
		}
		/// <summary>
		/// Adds the provided <see cref="HttpRequest"/> <c>InputStream</c> to the file cache.
		/// </summary>
		/// <param name="fileName">The name of the file being cached.</param>
		/// <param name="request">The <see cref="HttpRequest"/> to cache.</param>
		/// <returns>The absolute path the temp file.</returns>
		/// <exception cref="ArgumentNullException">The 'request' parameter is null.</exception>
		/// <exception cref="ArgumentOutOfRangeException">The 'request' <c>InputStream</c> is null or zero (0) length.</exception>
		public string Add(string fileName, HttpRequest request)
		{
			lock (GetFileLockObject(fileName))
			{
				if (null == request)
					throw new ArgumentNullException("request");
				var inputStream = request.InputStream;
				if (null == inputStream || inputStream.Length == 0)
					throw new ArgumentOutOfRangeException("request", "The HttpRequest InputStream is null or zero (0) length.");
				var path = Path.Combine(StoreDirectory, fileName);
				if (File.Exists(path))
				{
					File.Delete(path);
					Thread.Sleep(0);
				}
				request.SaveAs(path, false);
				return path;
			}
		}
		/// <summary>
		/// Determines if the provided file name exists in the cache.
		/// </summary>
		/// <param name="fileName">The name of the file to check for existence.</param>
		/// <returns>true if the file exists; otherwise false.</returns>
		/// <remarks>
		/// <para>
		/// Note: If the file exists in cache, the LastWriteTime is updated to the current time
		/// to ensure the file is not purged directly after a call this method.
		/// </para>
		/// </remarks>
		public bool Exists(string fileName)
		{
			lock (GetFileLockObject(fileName))
			{
				var filePath = Path.Combine(StoreDirectory, fileName);
				if (File.Exists(filePath))
				{
					// We extend the time this file will stay in cache
					// to ensure its not purged durinig file delivery
					File.SetLastWriteTime(filePath, DateTime.Now);
					return true;
				}
				return false;
			}
		}
		/// <summary>
		/// Gets the absolute path to a file contained in the cache.
		/// </summary>
		/// <param name="fileName">The name of the file to retrieve.</param>
		/// <returns>The absolute path of the requested file if it exists; otherwise an empty string.</returns>
		/// <remarks>
		/// <para>
		/// Note: If the file exists in cache, the LastWriteTime is updated to the current time
		/// to ensure the file is not purged directly after a call this method.
		/// </para>
		/// </remarks>
		public string GetFilePath(string fileName)
		{
			lock (GetFileLockObject(fileName))
			{
				var filePath = Path.Combine(StoreDirectory, fileName);
				if (File.Exists(filePath))
				{
					// We extend the time this file will stay in cache
					// to ensure its not purged durinig file delivery
					File.SetLastWriteTime(filePath, DateTime.Now);
					return filePath;
				}
				return string.Empty;
			}
		}
		/// <summary>
		/// Trys to get the absolute path for the requested file if it exists.
		/// </summary>
		/// <param name="fileName">The name of the file to retrieve.</param>
		/// <param name="filePath">An OUT parameter that will contain the absolute path of the requested file if it exists in the cache; otherwise an empty string.</param>
		/// <returns>true if the file exists and the absolute path resolved to an actual file on disk; otherwise false.</returns>
		/// <remarks>
		/// <para>
		/// Note: If the file exists in cache, the LastWriteTime is updated to the current time
		/// to ensure the file is not purged directly after a call this method.
		/// </para>
		/// </remarks>
		public bool TryGetFilePath(string fileName, out string filePath)
		{
			lock (GetFileLockObject(fileName))
			{
				filePath = string.Empty;
				try
				{
					filePath = Path.Combine(StoreDirectory, fileName);
					if (File.Exists(filePath))
					{
						// We extend the time this file will stay in cache
						// to ensure its not purged durinig file delivery
						File.SetLastWriteTime(filePath, DateTime.Now);
						return true;
					}
					return false;
				}
				catch { }
				return false;
			}
		}
		/// <summary>
		/// Enumerates the files currently in the cache.
		/// </summary>
		/// <returns>An enumerable collection of file names in the cache.</returns>
		/// <remarks>
		/// <para>
		/// NOTE: due to auto purging, there is no gaurantee that a given file exists immediately after a call to this method and
		/// you should confirm file existence before taking further action against any file found.
		/// </para>
		/// </remarks>
		public IEnumerable<string> EnumerateCache()
		{
			if (!Directory.Exists(this.StoreDirectory))
				return Enumerable.Empty<string>();
			return Directory.EnumerateFiles(this.StoreDirectory, "*.*", SearchOption.TopDirectoryOnly);
		}
		/// <summary>
		/// Requests a purge cycle to be processed.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This doesn't gaurentee a purge cycle happens. Merely attempts to queue up
		/// the request if one is not already queued.
		/// </para>
		/// </remarks>
		public void QueueAutoPurge()
		{
			PrivateQueueAutoPurge(DateTime.Now.Subtract(this.LastPurge.AddYears(1)));
		}
		#endregion

		#region Private

		private void PrivateQueueAutoPurge(TimeSpan lastPurge)
		{
			if (!purgeQueued && lastPurge > PurgeInterval)
			{
				lock (purgeQueuedLock)
				{
					if (!purgeQueued)
					{
						purgeQueued = true;
						ThreadPool.QueueUserWorkItem(PurgeCallback);
					}
				}
			}
		}
		private void PurgeCallback(object target)
		{
			var di = new DirectoryInfo(this.StoreDirectory);
			if (!di.Exists)
				return;

			var threshold = DateTime.Now.Subtract(this.PurgeInterval);
			var toTryDeleteAgain = new List<FileInfo>();
			var files = from file in di.EnumerateFiles("*.*", SearchOption.TopDirectoryOnly)
						select file;

			#region First Attempt

			foreach (var file in files)
			{
				if (file.CreationTime < threshold)
				{
					var fileName = file.Name;
					var lockObject = this.GetFileLockObject(fileName);
					if (null != lockObject)
					{
						if (!Monitor.TryEnter(lockObject))
						{
							toTryDeleteAgain.Add(file);
							continue;
						}

						try
						{
							file.Delete();
							this.DiscardFileLockObject(fileName);
						}
						catch (Exception)
						{
							// do nothing
						}
						finally
						{
							Monitor.Exit(lockObject);
						}
					}
				}
			}
			#endregion

			Thread.Sleep(0);

			#region Try again

			foreach (var fileinfo in toTryDeleteAgain)
			{
				var fileName = fileinfo.Name;
				var lockObject = GetFileLockObject(fileName);
				if (!Monitor.TryEnter(lockObject))
				{
					continue;
				}
				try
				{
					fileinfo.Delete();
					DiscardFileLockObject(fileName);
				}
				catch (Exception)
				{
					// do nothing, delete will be tried next time purge is called
				}
				finally
				{
					Monitor.Exit(lockObject);
				}
			}

			#endregion

			LastPurge = DateTime.Now;
			purgeQueued = false;
		}
		private object GetFileLockObject(string fileName)
		{
			object lockObject = fileLocks[fileName];

			if (lockObject == null)
			{
				// lock on the hashtable to prevent other writers
				lock (fileLocks)
				{
					lockObject = new object();
					fileLocks[fileName] = lockObject;
				}
			}

			return lockObject;
		}
		private void DiscardFileLockObject(string fileName)
		{
			lock (fileLocks)
				fileLocks.Remove(fileName);
		}
		#endregion

		#endregion
	}
	#endregion
}